import {
  ActionType,
  assertEqual,
  buildBlockActionKey,
  FlowError,
  FlowRunResponse,
  FlowRunStatus,
  GenericStepOutput,
  isNil,
  LoopStepOutput,
  LoopStepResult,
  PauseMetadata,
  spreadIfDefined,
  StepOutput,
  StepOutputStatus,
  StopResponse,
} from '@openops/shared';
import { nanoid } from 'nanoid';
import { StepExecutionPath } from './step-execution-path';

export enum ExecutionVerdict {
  RUNNING = 'RUNNING',
  PAUSED = 'PAUSED',
  SUCCEEDED = 'SUCCEEDED',
  FAILED = 'FAILED',
}

export enum VerdictReason {
  STOPPED = 'STOPPED',
  PAUSED = 'PAUSED',
  INTERNAL_ERROR = 'INTERNAL_ERROR',
  EXECUTION_LIMIT_REACHED = 'EXECUTION_LIMIT_REACHED',
}

export type VerdictResponse =
  | {
      reason: VerdictReason.PAUSED;
      pauseMetadata: PauseMetadata;
    }
  | {
      reason: VerdictReason.STOPPED;
      stopResponse: StopResponse;
    }
  | {
      reason: VerdictReason.INTERNAL_ERROR;
    }
  | {
      reason: VerdictReason.EXECUTION_LIMIT_REACHED;
      message: string;
    };

export class FlowExecutorContext {
  tasks: number;
  tags: readonly string[];
  steps: Readonly<Record<string, StepOutput>>;
  pauseId: string;
  verdict: ExecutionVerdict;
  verdictResponse: VerdictResponse | undefined;
  currentPath: StepExecutionPath;
  error?: FlowError;
  actionExecutionCounts: Readonly<Record<string, number>>;

  /**
   * Execution time in milliseconds
   */
  duration: number;

  constructor(copyFrom?: FlowExecutorContext) {
    this.tasks = copyFrom?.tasks ?? 0;
    this.tags = copyFrom?.tags ?? [];
    this.steps = copyFrom?.steps ?? {};
    this.pauseId = copyFrom?.pauseId ?? nanoid();
    this.duration = copyFrom?.duration ?? 0;
    this.verdict = copyFrom?.verdict ?? ExecutionVerdict.RUNNING;
    this.verdictResponse = copyFrom?.verdictResponse ?? undefined;
    this.error = copyFrom?.error ?? undefined;
    this.currentPath = copyFrom?.currentPath ?? StepExecutionPath.empty();
    this.actionExecutionCounts = copyFrom?.actionExecutionCounts ?? {};
  }

  static empty(): FlowExecutorContext {
    return new FlowExecutorContext();
  }

  public setPauseId(pauseId: string): FlowExecutorContext {
    return new FlowExecutorContext({
      ...this,
      pauseId,
    });
  }

  public getLoopStepOutput({
    stepName,
  }: {
    stepName: string;
  }): LoopStepOutput | undefined {
    const stateAtPath = getStateAtPath({
      currentPath: this.currentPath,
      steps: this.steps,
    });
    const stepOutput = stateAtPath[stepName];
    if (isNil(stepOutput)) {
      return undefined;
    }
    assertEqual(
      stepOutput.type,
      ActionType.LOOP_ON_ITEMS,
      'stepOutput.type',
      'LOOP_ON_ITEMS',
    );
    // The new LoopStepOutput is needed as casting directly to LoopClassOutput will just cast the data but the class methods will not be available
    return new LoopStepOutput(
      stepOutput as GenericStepOutput<ActionType.LOOP_ON_ITEMS, LoopStepResult>,
    );
  }

  public isCompleted({ stepName }: { stepName: string }): boolean {
    const stateAtPath = getStateAtPath({
      currentPath: this.currentPath,
      steps: this.steps,
    });
    const stepOutput = stateAtPath[stepName];
    if (isNil(stepOutput)) {
      return false;
    }
    return stepOutput.status !== StepOutputStatus.PAUSED;
  }

  public isPaused({ stepName }: { stepName: string }): boolean {
    const stateAtPath = getStateAtPath({
      currentPath: this.currentPath,
      steps: this.steps,
    });
    const stepOutput = stateAtPath[stepName];
    if (isNil(stepOutput)) {
      return false;
    }
    return stepOutput.status === StepOutputStatus.PAUSED;
  }

  public updateDuration(duration: number): FlowExecutorContext {
    const newDuration = duration + this.duration;
    return new FlowExecutorContext({
      ...this,
      duration: newDuration,
    });
  }

  public addTags(tags: string[]): FlowExecutorContext {
    return new FlowExecutorContext({
      ...this,
      tags: [...this.tags, ...tags].filter((value, index, self) => {
        return self.indexOf(value) === index;
      }),
    });
  }

  public increaseTask(tasks = 1): FlowExecutorContext {
    return new FlowExecutorContext({
      ...this,
      tasks: this.tasks + tasks,
    });
  }

  public upsertStep(
    stepName: string,
    stepOutput: StepOutput,
  ): FlowExecutorContext {
    const steps = {
      ...this.steps,
    };
    const targetMap = getStateAtPath({ currentPath: this.currentPath, steps });
    targetMap[stepName] = stepOutput;

    const error =
      stepOutput.status === StepOutputStatus.FAILED ||
      stepOutput.status === StepOutputStatus.EXECUTION_LIMIT_REACHED
        ? { stepName, message: stepOutput.errorMessage }
        : this.error;

    return new FlowExecutorContext({
      ...this,
      tasks: this.tasks,
      ...spreadIfDefined('error', error),
      steps,
    });
  }

  public getStepOutput(stepName: string): StepOutput | undefined {
    const stateAtPath = getStateAtPath({
      currentPath: this.currentPath,
      steps: this.steps,
    });
    return stateAtPath[stepName];
  }

  public setStepDuration({
    stepName,
    duration,
  }: SetStepDurationParams): FlowExecutorContext {
    const steps = {
      ...this.steps,
    };

    const targetMap = getStateAtPath({
      steps,
      currentPath: this.currentPath,
    });

    const stepOutput = targetMap[stepName];

    if (isNil(stepOutput)) {
      console.error(
        `[FlowExecutorContext#setStepDuration] Step ${stepName} not found in current path`,
      );
      return this;
    }

    targetMap[stepName].duration = duration;

    return new FlowExecutorContext({
      ...this,
      steps,
    });
  }

  public setCurrentPath(
    currentStatePath: StepExecutionPath,
  ): FlowExecutorContext {
    return new FlowExecutorContext({
      ...this,
      currentPath: currentStatePath,
    });
  }

  public setVerdict(
    verdict: ExecutionVerdict,
    response?: VerdictResponse,
  ): FlowExecutorContext {
    return new FlowExecutorContext({
      ...this,
      verdict,
      verdictResponse: response,
    });
  }

  public setRetryable(retryable: boolean): FlowExecutorContext {
    return new FlowExecutorContext({
      ...this,
      retryable,
    });
  }

  private static getActionKey(blockName: string, actionName: string): string {
    return buildBlockActionKey(blockName, actionName);
  }

  public incrementActionExecutionCount(
    blockName: string,
    actionName: string,
  ): FlowExecutorContext {
    const key = FlowExecutorContext.getActionKey(blockName, actionName);
    const currentCount = this.actionExecutionCounts[key] ?? 0;
    return new FlowExecutorContext({
      ...this,
      actionExecutionCounts: {
        ...this.actionExecutionCounts,
        [key]: currentCount + 1,
      },
    });
  }

  public getActionExecutionCount(
    blockName: string,
    actionName: string,
  ): number {
    const key = FlowExecutorContext.getActionKey(blockName, actionName);
    return this.actionExecutionCounts[key] ?? 0;
  }

  private parseErrorMessage(
    errorMessage: string | undefined,
    fallback?: string,
  ): string | undefined {
    if (!errorMessage) {
      return fallback;
    }
    try {
      const parsed = JSON.parse(errorMessage);
      return parsed.message || errorMessage;
    } catch {
      return errorMessage;
    }
  }

  public async toResponse(): Promise<FlowRunResponse> {
    const baseExecutionOutput = {
      duration: this.duration,
      tasks: this.tasks,
      tags: [...this.tags],
      steps: this.steps,
    };
    switch (this.verdict) {
      case ExecutionVerdict.FAILED: {
        const verdictResponse = this.verdictResponse;
        if (verdictResponse?.reason === VerdictReason.INTERNAL_ERROR) {
          return {
            ...baseExecutionOutput,
            error: this.error,
            status: FlowRunStatus.INTERNAL_ERROR,
            terminationReason: 'Flow execution encountered an internal error',
          };
        }
        if (verdictResponse?.reason === VerdictReason.EXECUTION_LIMIT_REACHED) {
          return {
            ...baseExecutionOutput,
            error: this.error,
            status: FlowRunStatus.STOPPED,
            terminationReason: this.parseErrorMessage(
              this.error?.message,
              'Test run execution limit reached',
            ),
          };
        }
        return {
          ...baseExecutionOutput,
          error: this.error,
          status: FlowRunStatus.FAILED,
          terminationReason: this.parseErrorMessage(this.error?.message),
        };
      }
      case ExecutionVerdict.PAUSED: {
        const verdictResponse = this.verdictResponse;
        if (verdictResponse?.reason !== VerdictReason.PAUSED) {
          throw new Error(
            'Verdict Response should have pause metadata response',
          );
        }
        return {
          ...baseExecutionOutput,
          status: FlowRunStatus.PAUSED,
          pauseMetadata: verdictResponse.pauseMetadata,
        };
      }
      case ExecutionVerdict.RUNNING: {
        return {
          ...baseExecutionOutput,
          status: FlowRunStatus.RUNNING,
        };
      }
      case ExecutionVerdict.SUCCEEDED: {
        const verdictResponse = this.verdictResponse;
        if (verdictResponse?.reason === VerdictReason.STOPPED) {
          return {
            ...baseExecutionOutput,
            status: FlowRunStatus.SUCCEEDED,
            stopResponse: verdictResponse.stopResponse,
          };
        }
        return {
          ...baseExecutionOutput,
          status: FlowRunStatus.SUCCEEDED,
        };
      }
    }
  }
  public currentState(): Record<string, unknown> {
    let flattenedSteps: Record<string, unknown> = extractOutput(this.steps);
    let targetMap = this.steps;
    this.currentPath.path.forEach(([stepName, iteration]) => {
      const stepOutput = targetMap[stepName];
      if (!stepOutput.output || stepOutput.type !== ActionType.LOOP_ON_ITEMS) {
        throw new Error(
          '[ExecutionState#getTargetMap] Not instance of Loop On Items step output',
        );
      }
      targetMap = stepOutput.output.iterations[iteration];
      flattenedSteps = {
        ...flattenedSteps,
        ...extractOutput(targetMap),
      };
    });
    return flattenedSteps;
  }
}

function extractOutput(
  steps: Record<string, StepOutput>,
): Record<string, unknown> {
  return Object.entries(steps).reduce(
    (acc: Record<string, unknown>, [stepName, step]) => {
      acc[stepName] = step.output;
      return acc;
    },
    {} as Record<string, unknown>,
  );
}

function getStateAtPath({
  currentPath,
  steps,
}: {
  currentPath: StepExecutionPath;
  steps: Record<string, StepOutput>;
}): Record<string, StepOutput> {
  let targetMap = steps;
  currentPath.path.forEach(([stepName, iteration]) => {
    const stepOutput = targetMap[stepName];
    if (!stepOutput.output || stepOutput.type !== ActionType.LOOP_ON_ITEMS) {
      throw new Error(
        '[ExecutionState#getTargetMap] Not instance of Loop On Items step output',
      );
    }
    targetMap = stepOutput.output.iterations[iteration];
  });
  return targetMap;
}

type SetStepDurationParams = {
  stepName: string;
  duration: number;
};
